-- -------------------------------------------------------------------------
-- Features/Player.lua
-- Online Player List & Interactions
-- -------------------------------------------------------------------------

local module = {}

local ui, natives, input_module

-- [Natives Cache]
local N = {}

local table_insert <const> = table.insert
local table_sort <const> = table.sort
local os_clock <const> = os.clock
local pcall <const> = pcall
local pairs <const> = pairs
local ipairs <const> = ipairs

local function player_sort_by_id(a, b)
    return (a.id or 0) < (b.id or 0)
end

local global_T = nil
local function _T(text)
    if global_T then return global_T(text) end
    if _G._T then global_T = _G._T; return global_T(text) end
    return text
end

local UPDATE_INTERVAL <const> = 0.25
local CACHE_DURATION <const> = 5.0
local TELEPORT_Z_OFFSET <const> = 5.0
local SPECTATE_FREEZE_INTERVAL <const> = 2.0

local player_obj_pool = {}
local tag_pool = {}
local cached_player_list = {}

local function acquire_table(pool)
    local t = table.remove(pool)
    if t then
        for i = #t, 1, -1 do
            t[i] = nil
        end
        for k in pairs(t) do
            t[k] = nil
        end
        return t
    end
    return {}
end

local function release_table(pool, t)
    if t then table_insert(pool, t) end
end

local state = {
    selected_player_id = nil,
    player_action_tab = 1,
    player_list = {},
    player_list_timer = 0,
    player_lookup = {},
    player_count = 0,
    spectating_id = nil,
    spectate_saved_coords = nil,
    spectate_freeze_timer = 0,
    next_host_id = -1,
    my_id = -1,
    my_id_timer = 0,
    script_host_id = -1,
    script_host_timer = 0,
    session_host_id = -1
}

local T_ME = {key="ME", fallback="ME", color="Tag ME"}
local T_FRIEND = {key="Friend", fallback="Friend", color="Tag Friend"}
local T_MOD = {key="Modder", fallback="Modder", color="Tag Modder"}
local T_SPEC = {key="Spectating", fallback="Spectating", color="Tag Spectating"}
local T_SCR_HOST = {key="SCRIPT HOST", fallback="Script Host", color="Tag Script Host"}
local T_SES_HOST = {key="SESSION HOST", fallback="Session Host", color="Tag Session Host"}
local T_NEXT = {key="NEXT HOST", fallback="Next Host", color="Tag Next Host"}
local T_CEO = {key="CEO", fallback="CEO", color="Tag CEO"}
local T_INT = {key="Interior", fallback="Interior", color="Tag Interior"}
local T_DEAD = {key="Dead", fallback="Dead", color="Tag Dead"}
local T_VEH = {key="Vehicle", fallback="Vehicle", color="Tag Vehicle"}
local T_GOD = {key="God", fallback="God", color="Tag God"}
local T_BOUNTY = {key="Bounty", fallback="Bounty", color="Tag Bounty"}
local T_MODDED = {key="Modded", fallback="Modded", color="Tag Modded"}
local T_CTRL = {key="Controller", fallback="Controller", color="Tag Controller"}

local function resolve_tag_text(tag)
    if not tag then return "" end
    if tag.key then
        local t = _T(tag.key)
        if tag.fallback and t == tag.key then
            return tag.fallback
        end
        return t
    end
    return _T(tag.text or "")
end

local function push_tag(tags, tag)
    tag.text = resolve_tag_text(tag)
    table_insert(tags, tag)
end

function module.get_selected_player_id() return state.selected_player_id end
function module.get_host_ids() return state.script_host_id, state.session_host_id end

local function show_toast(text, type_str)
    if ui and ui.notify then ui.notify(text, type_str or "Info")
    elseif util and util.toast then util.toast(text) end
end

local function get_coords(pid, p_obj)
    if p_obj and p_obj.coords and p_obj.coords.x then return p_obj.coords end
    local p = players.get(pid)
    return (p and p.coords and p.coords.x) and p.coords or nil
end

local function get_ped(pid, p_obj)
    if p_obj and p_obj.ped then return p_obj.ped end
    local p = players.get(pid)
    return (p and p.ped) and p.ped or nil
end

local function stop_spectating()
    if state.spectating_id == nil then return end
    local me = players.me()
    local my_ped = me and me.ped

    if my_ped then
        pcall(function()
            if N.NETWORK_SET_IN_SPECTATOR_MODE then N.NETWORK_SET_IN_SPECTATOR_MODE(false, my_ped) end
            if N.FREEZE_ENTITY_POSITION then N.FREEZE_ENTITY_POSITION(my_ped, false) end
            if state.spectate_saved_coords then
                local c = state.spectate_saved_coords
                if N.SET_ENTITY_COORDS_NO_OFFSET then
                    N.SET_ENTITY_COORDS_NO_OFFSET(my_ped, c.x, c.y, c.z, false, false, false)
                end
                state.spectate_saved_coords = nil
            end
        end)
    end
    state.spectating_id = nil
end

local function update_caches()
    local now = os_clock()
    if now > state.my_id_timer then
        local me = players.me()
        state.my_id = me and me.id or -1
        state.my_id_timer = now + CACHE_DURATION
    end
    if now > state.script_host_timer then
        state.script_host_id = -1
        if script and script.host then
            local sh = script.host("freemode")
            if sh then state.script_host_id = sh.id end
        end
        state.script_host_timer = now + 2.0
    end
end

local function get_tags(p, pid)
    local t = acquire_table(tag_pool)

    if pid == state.my_id then push_tag(t, T_ME) end
    if p.friend then push_tag(t, T_FRIEND) end
    if p.modder then push_tag(t, T_MOD) end
    if state.spectating_id == pid then push_tag(t, T_SPEC) end

    local is_sh = (pid == state.script_host_id)
    local is_fh = p.freemode_host or (state.session_host_id ~= -1 and pid == state.session_host_id)
    if is_sh then push_tag(t, T_SCR_HOST) end
    if is_fh then push_tag(t, T_SES_HOST) end

    if state.next_host_id == pid then push_tag(t, T_NEXT) end
    if p.boss or p.in_ceo then push_tag(t, T_CEO) end
    if p.in_interior then push_tag(t, T_INT) end

    if not p.alive then push_tag(t, T_DEAD)
    else
        if p.in_vehicle then
            push_tag(t, T_VEH)
        end
        if p.god then push_tag(t, T_GOD) end
    end

    if p.active_bounty or (p.bounty and p.bounty > 0) then push_tag(t, T_BOUNTY) end

    local rank = p.rank or 0
    local money = p.money or 0
    local bank = p.bank or 0
    if rank >= 1000 or money > 500000000 or bank > 500000000 then
        push_tag(t, T_MODDED)
    end

    if p.controller then push_tag(t, T_CTRL) end

    return t
end

local last_player_ids = {}
local list_dirty = true

local function update_list()
    if os_clock() < state.player_list_timer then return end
    update_caches()

    local session_host_id = -1
    if game and game.host then
        local host_player = game.host()
        if host_player and host_player.id then
            session_host_id = host_player.id
        end
    end
    state.next_host_id = -1

    local current_ids = {}
    local player_data = {}
    local has_changes = false

    for _, p in ipairs(players.list()) do
        if p and p.connected ~= false then
            local pid = p.id
            current_ids[pid] = true
            player_data[pid] = p

            if session_host_id == -1 and p.freemode_host then
                session_host_id = pid
            end

            if not last_player_ids[pid] then
                has_changes = true
            end

            if state.next_host_id == -1 and pid ~= session_host_id then
                state.next_host_id = pid
            end
        end
    end

    for pid in pairs(last_player_ids) do
        if not current_ids[pid] then
            has_changes = true
            break
        end
    end

    state.session_host_id = session_host_id

    if not has_changes and not list_dirty then
        for _, entry in ipairs(state.player_list) do
            local p = player_data[entry.id]
            if p then
                if entry.tags then release_table(tag_pool, entry.tags) end
                entry.tags = get_tags(p, entry.id)
                entry.obj = p
            end
        end
        state.player_list_timer = os_clock() + UPDATE_INTERVAL
        return
    end

    list_dirty = false
    last_player_ids = current_ids

    for _, p_entry in ipairs(state.player_list) do
        if p_entry.tags then release_table(tag_pool, p_entry.tags) end
        release_table(player_obj_pool, p_entry)
    end

    for k in pairs(cached_player_list) do
        cached_player_list[k] = nil
    end

    local old_lookup = state.player_lookup
    local list_idx = 1

    for pid, p in pairs(player_data) do
        local name = p.name or "**Unknown**"

        local lbl
        local old = old_lookup[pid]
        if old and old.name == name then
            lbl = old.display_label
        else
            lbl = name .. " [" .. pid .. "]"
        end

        local entry = acquire_table(player_obj_pool)
        entry.id = pid
        entry.name = name
        entry.display_label = lbl
        entry.tags = get_tags(p, pid)
        entry.obj = p

        cached_player_list[list_idx] = entry
        list_idx = list_idx + 1
    end

    table_sort(cached_player_list, player_sort_by_id)
    state.player_list = cached_player_list
    state.player_count = list_idx - 1

    state.player_lookup = {}
    for _, p in ipairs(cached_player_list) do state.player_lookup[p.id] = p end

    state.player_list_timer = os_clock() + UPDATE_INTERVAL
end

function module.init(ui_lib, _, nat_lib, _, _, _, _)
    ui, natives = ui_lib, nat_lib

    -- Cache natives
    if natives then
        N.NETWORK_SET_IN_SPECTATOR_MODE = natives.NETWORK_SET_IN_SPECTATOR_MODE
        N.FREEZE_ENTITY_POSITION = natives.FREEZE_ENTITY_POSITION
        N.SET_ENTITY_COORDS_NO_OFFSET = natives.SET_ENTITY_COORDS_NO_OFFSET
        N.GET_HASH_KEY = natives.GET_HASH_KEY
        N.GIVE_WEAPON_TO_PED = natives.GIVE_WEAPON_TO_PED
        N.ADD_EXPLOSION = natives.ADD_EXPLOSION
    end

    local ok, inp_mod = pcall(require, "ClickUl.UI.input")
    if ok then
        input_module = inp_mod
    end

    return true
end

function module.on_menu_open()
    state.player_list_timer = 0
    pcall(update_list)
end

function module.on_tick()
    if state.spectating_id then
        local now = os_clock()
        if now > state.spectate_freeze_timer then
            local me = players.me()
            local ped = me and me.ped
            if ped and N.FREEZE_ENTITY_POSITION then N.FREEZE_ENTITY_POSITION(ped, true) end
            state.spectate_freeze_timer = now + SPECTATE_FREEZE_INTERVAL
        end

        local t = players.get(state.spectating_id)
        if not t or not t.connected then
            stop_spectating()
            show_toast(_T("Target left session"), "Warning")
        end
    end
end

function module.draw()
    pcall(update_list)

    ui.DualColumn_FixedLeft(
        function()
            ui.ScrollGroup(_T("Online Players") .. " (" .. state.player_count .. "/32)###PlayerList", "auto", function()
                if #state.player_list == 0 then ui.Button(_T("No players found")) end

                for _, p in ipairs(state.player_list) do
                    local lbl = p.display_label
                    if state.selected_player_id == p.id then lbl = "> " .. lbl end
                    if ui.Selectable(lbl, state.selected_player_id == p.id, p.tags) then
                        state.selected_player_id = p.id
                    end
                end
            end)
        end,
        function()
            state.player_action_tab = ui.SubTabBar({_T("General"), _T("Teleport"), _T("Friendly"), _T("Trolling")}, state.player_action_tab or 1)

            local tName, tObj = nil, nil
            local valid = false
            if state.selected_player_id then
                local p = state.player_lookup[state.selected_player_id]
                if p then tName, tObj, valid = p.name, p.obj, true end
            end

            ui.Group("##PlayerActions", function()
                local tab = state.player_action_tab
                if tab == 1 then
                    local is_spec = (state.spectating_id == state.selected_player_id)
                    if ui.Checkbox(_T("Spectate Player"), is_spec) ~= is_spec then
                        if not valid then show_toast(_T("Select a player"), "Warning")
                        elseif not is_spec then
                            local ped = get_ped(state.selected_player_id, tObj)
                            if ped then
                                local me = players.me()
                                state.spectate_saved_coords = me.coords
                                if N.FREEZE_ENTITY_POSITION then N.FREEZE_ENTITY_POSITION(me.ped, true) end
                                if N.NETWORK_SET_IN_SPECTATOR_MODE then N.NETWORK_SET_IN_SPECTATOR_MODE(true, ped) end
                                state.spectating_id = state.selected_player_id
                            else
                                show_toast(_T("Entity invalid"), "Error")
                            end
                        else stop_spectating() end
                    end
                    ui.Row({
                        function()
                            if ui.Button(_T("Copy Name")) and valid then
                                if input_module and input_module.copy_to_clipboard then
                                    input_module.copy_to_clipboard(tName)
                                end
                                show_toast(_T("Copied name"), "Success")
                            end
                        end,
                        function()
                            if ui.Button(_T("Copy RID")) and valid then
                                local rid = tObj and tObj.rockstar_id
                                if rid and input_module and input_module.copy_to_clipboard then
                                    input_module.copy_to_clipboard(tostring(rid))
                                    show_toast(_T("Copied RID"), "Success")
                                end
                            end
                        end
                    })

                elseif tab == 2 then
                    if ui.Button(_T("Teleport To")) and valid then
                        local c = get_coords(state.selected_player_id, tObj)
                        if c then
                            local me = players.me()
                            local entity = (me.vehicle and me.vehicle ~= 0) and me.vehicle or me.ped
                            if N.SET_ENTITY_COORDS_NO_OFFSET then
                                N.SET_ENTITY_COORDS_NO_OFFSET(entity, c.x, c.y, c.z, false, false, false)
                                show_toast(_T("Teleported"), "Success")
                            end
                        else
                            show_toast(_T("Invalid coords"), "Error")
                        end
                    end
                    if ui.Button(_T("Teleport To Above")) and valid then
                        local c = get_coords(state.selected_player_id, tObj)
                        if c then
                            local me = players.me()
                            local entity = (me.vehicle and me.vehicle ~= 0) and me.vehicle or me.ped
                            if N.SET_ENTITY_COORDS_NO_OFFSET then
                                N.SET_ENTITY_COORDS_NO_OFFSET(entity, c.x, c.y, c.z + TELEPORT_Z_OFFSET, false, false, false)
                                show_toast(_T("Teleported above"), "Success")
                            end
                        end
                    end

                elseif tab == 3 then
                    if ui.Button(_T("Give Weapons")) and valid then
                        local ped = get_ped(state.selected_player_id, tObj)
                        if ped and N.GET_HASH_KEY and N.GIVE_WEAPON_TO_PED then
                            for _, w in ipairs({"WEAPON_PISTOL", "WEAPON_COMBATPISTOL", "WEAPON_ASSAULTRIFLE", "WEAPON_RPG"}) do
                                local h = N.GET_HASH_KEY(w)
                                N.GIVE_WEAPON_TO_PED(ped, h, 9999, false, true)
                            end
                            show_toast(_T("Given Weapons"), "Success")
                        end
                    end

                elseif tab == 4 then
                    if ui.Button(_T("Explode")) and valid then
                        local c = get_coords(state.selected_player_id, tObj)
                        if c and N.ADD_EXPLOSION then
                            N.ADD_EXPLOSION(c.x, c.y, c.z, 1, 1.0, true, false, 1.0, false)
                            show_toast(_T("Exploded"), "Warning")
                        end
                    end
                end
            end)
        end
    )
end

return module
